В вашем распоряжении есть:
Датасет final_ab_events.csv — c действиями новых пользователей в период с 7 декабря 2020 по 4 января 2021 года.
Техническое задание:
recommender_system_test;product_page,product_cart,purchase.ab_project_marketing_events.csv — календарь маркетинговых событий на 2020 год
final_ab_new_users.csv — пользователи, зарегистрировавшиеся с 7 по 21 декабря 2020 года
final_ab_participants.csv — таблица участников тестов
Цели исследования :
Оценить корректность проведения теста
Проанализировать результаты теста
Ход исследования
Провести обзор и предобработку данных
Оценить корректность проведения теста. Проверить:
Соответствие данных требованиям технического задания
Время проведения теста
Аудиторию теста
Провести исследовательский анализ данных
Проанализировать результаты теста
import pandas as pd # Импортируем необходимые библиотеки, прочитаем файл и сохраним его в переменную data.
import seaborn as sns
from matplotlib import pyplot as plt
from plotly import graph_objects as go
plt.style.use('ggplot')
import scipy.stats as st
from statsmodels. stats.proportion import proportions_ztest
import numpy as np
import math as mth
import datetime as dt
from pandas.plotting import register_matplotlib_converters
import warnings
register_matplotlib_converters()
campaings, new_users, events, participants = (
pd.read_csv('ab_project_marketing_events.csv'),
pd.read_csv('final_ab_new_users.csv'),
pd.read_csv('final_ab_events.csv'),
pd.read_csv('final_ab_participants.csv')
)
#функция для обзора данных
def overview_df(df):
'''Первый взгляд на данные'''
print('...........Первые 5 строк...........')
display(df.head())
print('')
print('')
print('...........Тип данных...........')
print('')
print(df.info())
overview_df(campaings)
...........Первые 5 строк...........
| name | regions | start_dt | finish_dt | |
|---|---|---|---|---|
| 0 | Christmas&New Year Promo | EU, N.America | 2020-12-25 | 2021-01-03 |
| 1 | St. Valentine's Day Giveaway | EU, CIS, APAC, N.America | 2020-02-14 | 2020-02-16 |
| 2 | St. Patric's Day Promo | EU, N.America | 2020-03-17 | 2020-03-19 |
| 3 | Easter Promo | EU, CIS, APAC, N.America | 2020-04-12 | 2020-04-19 |
| 4 | 4th of July Promo | N.America | 2020-07-04 | 2020-07-11 |
...........Тип данных........... <class 'pandas.core.frame.DataFrame'> RangeIndex: 14 entries, 0 to 13 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 name 14 non-null object 1 regions 14 non-null object 2 start_dt 14 non-null object 3 finish_dt 14 non-null object dtypes: object(4) memory usage: 576.0+ bytes None
Датасет campaings содержит календарь маркетинговых событий на 2020 год.
Структура файла:
name — название маркетингового события
regions — регионы, в которых будет проводиться рекламная кампания
start_dt — дата начала кампании
finish_dt — дата завершения кампании
Таблица содержит 14 строк и 4 столбца. Тип данных столбцов с датами необходимо изменить на datetime.
overview_df(new_users)
...........Первые 5 строк...........
| user_id | first_date | region | device | |
|---|---|---|---|---|
| 0 | D72A72121175D8BE | 2020-12-07 | EU | PC |
| 1 | F1C668619DFE6E65 | 2020-12-07 | N.America | Android |
| 2 | 2E1BF1D4C37EA01F | 2020-12-07 | EU | PC |
| 3 | 50734A22C0C63768 | 2020-12-07 | EU | iPhone |
| 4 | E1BDDCE0DAFA2679 | 2020-12-07 | N.America | iPhone |
...........Тип данных........... <class 'pandas.core.frame.DataFrame'> RangeIndex: 61733 entries, 0 to 61732 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 61733 non-null object 1 first_date 61733 non-null object 2 region 61733 non-null object 3 device 61733 non-null object dtypes: object(4) memory usage: 1.9+ MB None
Датасет new_users - все пользователи, зарегистрировавшиеся в интернет-магазине в период с 7 по 21 декабря 2020 года.
Структура файла:
user_id — идентификатор пользователя
first_date — дата регистрации
region — регион пользователя
device — устройство, с которого происходила регистрация
Таблица содержит 61733 строки и 4 столбца. Тип данных столбца с датами необходимо изменить на datetime.
overview_df(events)
...........Первые 5 строк...........
| user_id | event_dt | event_name | details | |
|---|---|---|---|---|
| 0 | E1BDDCE0DAFA2679 | 2020-12-07 20:22:03 | purchase | 99.99 |
| 1 | 7B6452F081F49504 | 2020-12-07 09:22:53 | purchase | 9.99 |
| 2 | 9CD9F34546DF254C | 2020-12-07 12:59:29 | purchase | 4.99 |
| 3 | 96F27A054B191457 | 2020-12-07 04:02:40 | purchase | 4.99 |
| 4 | 1FD7660FDF94CA1F | 2020-12-07 10:15:09 | purchase | 4.99 |
...........Тип данных........... <class 'pandas.core.frame.DataFrame'> RangeIndex: 440317 entries, 0 to 440316 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 440317 non-null object 1 event_dt 440317 non-null object 2 event_name 440317 non-null object 3 details 62740 non-null float64 dtypes: float64(1), object(3) memory usage: 13.4+ MB None
Датасет events — все события новых пользователей в период с 7 декабря 2020 по 4 января 2021 года.
Структура файла:
user_id — идентификатор пользователя
event_dt — дата и время события
event_name — тип события
details — дополнительные данные о событии. Например, для покупок, purchase, в этом поле хранится стоимость покупки в долларах
Таблица содержит 440317 строки и 4 столбца. Наблюдаются пропуски в столбце details. Тип данных столбца с датами необходимо изменить на datetime.
overview_df(participants)
...........Первые 5 строк...........
| user_id | group | ab_test | |
|---|---|---|---|
| 0 | D1ABA3E2887B6A73 | A | recommender_system_test |
| 1 | A7A3664BD6242119 | A | recommender_system_test |
| 2 | DABC14FDDFADD29E | A | recommender_system_test |
| 3 | 04988C5DF189632E | A | recommender_system_test |
| 4 | 482F14783456D21B | B | recommender_system_test |
...........Тип данных........... <class 'pandas.core.frame.DataFrame'> RangeIndex: 18268 entries, 0 to 18267 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 18268 non-null object 1 group 18268 non-null object 2 ab_test 18268 non-null object dtypes: object(3) memory usage: 428.3+ KB None
Датасет participants — таблица участников тестов.
Структура файла:
user_id — идентификатор пользователя
ab_test — название теста
group — группа пользователя
Таблица содержит 18268 строк и 3 столбца.
Столбцы с датами необходимо перевести в тип datetime для удобства работы с данными. Изучить пропуски в одном из столбцов.
Поменяем тип данных столбцов с временем на datetime.
campaings['start_dt'] = pd.to_datetime(campaings['start_dt'])
campaings['finish_dt'] = pd.to_datetime(campaings['finish_dt'])
new_users['first_date'] = pd.to_datetime(new_users['first_date'])
events['event_dt'] = pd.to_datetime(events['event_dt'])
events.isna().agg(['mean', 'sum']).T.sort_values(by='mean', ascending=False) # посмотрим доли пропусков
| mean | sum | |
|---|---|---|
| details | 0.857512 | 377577.0 |
| user_id | 0.000000 | 0.0 |
| event_dt | 0.000000 | 0.0 |
| event_name | 0.000000 | 0.0 |
В столбце details датафрейма events почти 86 % пропусков. В документации сказано, что в этом столбце хранятся дополнительные данные о событии. Например, для покупок, purchase, в этом поле хранится стоимость покупки в долларах. Это значит, что значения в details зависят от столбца с названиями событий. Посмотрим каким событиям соответствуют пропуски.
events.query("details.isna()").value_counts(['event_name']) # количесвто соответствующих событий с пропусками в details
event_name login 189552 product_page 125563 product_cart 62462 dtype: int64
events.value_counts(['event_name']) # количество событий в общем
event_name login 189552 product_page 125563 purchase 62740 product_cart 62462 dtype: int64
Можно сделать вывод, что в столбце details заполнены значения только по стоимости покупки. Соответственно дополнительных данных для событий-авторизация, карточка товара или продуктовая страница не предусмотрено. Соотвественно оставим пропуски.
Проверим датафреймы на яные дубликаты.
campaings.duplicated().sum()
0
new_users.duplicated().sum()
0
events.duplicated().sum()
0
participants.duplicated().sum()
0
Явных дубликатов не обнаружено.
Посмотрим уникальные значения столбцов и проверим их на неявные дубликаты.
campaings['name'].value_counts()
Christmas&New Year Promo 1 St. Valentine's Day Giveaway 1 St. Patric's Day Promo 1 Easter Promo 1 4th of July Promo 1 Black Friday Ads Campaign 1 Chinese New Year Promo 1 Labor day (May 1st) Ads Campaign 1 International Women's Day Promo 1 Victory Day CIS (May 9th) Event 1 CIS New Year Gift Lottery 1 Dragon Boat Festival Giveaway 1 Single's Day Gift Promo 1 Chinese Moon Festival 1 Name: name, dtype: int64
В значениях присутствуют данные о названиях кампаний, неявных дубликатов не наблюдается.
campaings['regions'].value_counts()
APAC 4 EU, CIS, APAC, N.America 3 EU, N.America 2 EU, CIS, APAC 2 CIS 2 N.America 1 Name: regions, dtype: int64
В значениях присутствуют данные о проведении кампаний в регионах и их сочетаниях. Неявных дубликатов не выявлено.
new_users['region'].value_counts()
EU 46270 N.America 9155 CIS 3155 APAC 3153 Name: region, dtype: int64
Представлено 4 региона новых пользователей.
new_users['device'].value_counts()
Android 27520 PC 15599 iPhone 12530 Mac 6084 Name: device, dtype: int64
Представлено 4 устройства, с которых происходила регистрация.
events['event_name'].value_counts()
login 189552 product_page 125563 purchase 62740 product_cart 62462 Name: event_name, dtype: int64
В датафрейме с событиями пользователей представлено 4 вида событий: регистрация, просмотр корзины, конверсии в просмотр карточек товаров и покупка. Заменим именования событий для удобства восприятия.
def new_name(name):
if name == 'login':
return 'регистрация'
elif name == 'product_page':
return 'конверсии в просмотр карточек товаров'
elif name == 'purchase':
return 'покупка'
elif name == 'product_cart':
return 'просмотр корзины'
else:
return 'Unknown'
events['event_name'] = events['event_name'].apply(new_name)
events.head()
| user_id | event_dt | event_name | details | |
|---|---|---|---|---|
| 0 | E1BDDCE0DAFA2679 | 2020-12-07 20:22:03 | покупка | 99.99 |
| 1 | 7B6452F081F49504 | 2020-12-07 09:22:53 | покупка | 9.99 |
| 2 | 9CD9F34546DF254C | 2020-12-07 12:59:29 | покупка | 4.99 |
| 3 | 96F27A054B191457 | 2020-12-07 04:02:40 | покупка | 4.99 |
| 4 | 1FD7660FDF94CA1F | 2020-12-07 10:15:09 | покупка | 4.99 |
participants['group'].value_counts()
A 9655 B 8613 Name: group, dtype: int64
В датафрейме с данными участников теста представлено разделение на 2 группы - А и В.
participants['ab_test'].value_counts()
interface_eu_test 11567 recommender_system_test 6701 Name: ab_test, dtype: int64
Также мы наблюдаем 2 вида теста : так называемый наш тест по ТЗ о внедрении улучшенной рекомендательной системы. И тест связанный с изменениями с интерфейсом, проводимый с европейской аудиторией.
print('В campaings представлены данные о начале и завершении маркетинговых событий за период:', campaings['finish_dt'].max()-campaings['start_dt'].min(),
'с', campaings['start_dt'].min(), 'по', campaings['finish_dt'].max())
В campaings представлены данные о начале и завершении маркетинговых событий за период: 348 days 00:00:00 с 2020-01-25 00:00:00 по 2021-01-07 00:00:00
print('В new_users представлены данные регистрации новых пользователей за период:', new_users['first_date'].max()-new_users['first_date'].min(),
'с', new_users['first_date'].min(), 'по', new_users['first_date'].max())
В new_users представлены данные регистрации новых пользователей за период: 16 days 00:00:00 с 2020-12-07 00:00:00 по 2020-12-23 00:00:00
print('В events представлены данные о событиях новых пользователей за период:', events['event_dt'].max()-events['event_dt'].min(),
'с', events['event_dt'].min(), 'по', events['event_dt'].max())
В events представлены данные о событиях новых пользователей за период: 23 days 23:36:00 с 2020-12-07 00:00:33 по 2020-12-30 23:36:33
new_users['user_id'].nunique()
61733
Столбец с идентификаторами пользователей содержит уникальные значения в каждой строке.
events['user_id'].nunique()
58703
Мы делаем вывод, что не все пользователи совершали события, так как в таблице с событиями уникальных id меньше.
participants['user_id'].nunique()
16666
Таблица participants содержит 18268 строк, уникальных id она содержит в меньшем количестве, соответственно, делаем вывод, что некоторые участники тестов попали в оба теста и повторились.
Поменяли тип данных столбцов с временем на datetime
в столбце details заполнены значения только по стоимости покупки. Данных для остальных событий не предусмотрено. Соотвественно пропуски оставили.
Название теста по ТЗ recommender_system_test. Проверим соответствие данных ТЗ по всем участникам данного теста. Отберем участников нашего теста из таблицы participants.
rec_participants = participants.query('ab_test == "recommender_system_test"')
rec_participants
| user_id | group | ab_test | |
|---|---|---|---|
| 0 | D1ABA3E2887B6A73 | A | recommender_system_test |
| 1 | A7A3664BD6242119 | A | recommender_system_test |
| 2 | DABC14FDDFADD29E | A | recommender_system_test |
| 3 | 04988C5DF189632E | A | recommender_system_test |
| 4 | 482F14783456D21B | B | recommender_system_test |
| ... | ... | ... | ... |
| 6696 | 053FB26D6D49EDDC | A | recommender_system_test |
| 6697 | 9D263B8EF15CF188 | B | recommender_system_test |
| 6698 | F2FBBA33F37DEC46 | A | recommender_system_test |
| 6699 | 29C92313A98B1176 | B | recommender_system_test |
| 6700 | 6715343AFBA285AE | B | recommender_system_test |
6701 rows × 3 columns
В тесте recommender_system_test участвовал 6701 пользователь. Ожидаемое количество участников теста по ТЗ - 6000.
rec_participants.groupby(['ab_test', 'group']).agg({'user_id': 'nunique'})
| user_id | ||
|---|---|---|
| ab_test | group | |
| recommender_system_test | A | 3824 |
| B | 2877 |
Распределение по группам выглядит неравномерным. Разница почти в 1000 пользователей.
Проверим соответствие требованиям ТЗ по датам :
дата запуска: 2020-12-07;
дата остановки набора новых пользователей: 2020-12-21;
дата остановки: 2021-01-04;
Соединим таблицы по id пользователей и сверим даты с ТЗ.
new_rec_participants = rec_participants.merge(new_users, on='user_id')
new_rec_participants
| user_id | group | ab_test | first_date | region | device | |
|---|---|---|---|---|---|---|
| 0 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-07 | EU | PC |
| 1 | A7A3664BD6242119 | A | recommender_system_test | 2020-12-20 | EU | iPhone |
| 2 | DABC14FDDFADD29E | A | recommender_system_test | 2020-12-08 | EU | Mac |
| 3 | 04988C5DF189632E | A | recommender_system_test | 2020-12-14 | EU | iPhone |
| 4 | 482F14783456D21B | B | recommender_system_test | 2020-12-14 | EU | PC |
| ... | ... | ... | ... | ... | ... | ... |
| 6696 | 053FB26D6D49EDDC | A | recommender_system_test | 2020-12-10 | N.America | Android |
| 6697 | 9D263B8EF15CF188 | B | recommender_system_test | 2020-12-16 | N.America | Mac |
| 6698 | F2FBBA33F37DEC46 | A | recommender_system_test | 2020-12-18 | APAC | Mac |
| 6699 | 29C92313A98B1176 | B | recommender_system_test | 2020-12-07 | APAC | Android |
| 6700 | 6715343AFBA285AE | B | recommender_system_test | 2020-12-07 | CIS | Android |
6701 rows × 6 columns
new_rec_participants = new_rec_participants.merge(events, on='user_id', how='left')
new_rec_participants['user_id'].nunique()
6701
Проверим дату регистрации.
print('Участники recommender_system_test регистрировались в период:', new_rec_participants['first_date'].max()-new_rec_participants['first_date'].min(),
'с', new_rec_participants['first_date'].min(), 'по', new_rec_participants['first_date'].max())
Участники recommender_system_test регистрировались в период: 14 days 00:00:00 с 2020-12-07 00:00:00 по 2020-12-21 00:00:00
Максимальная дата регистрации новых пользователей соответствует дате окончания набора новых пользователей. Минимальная дата, соответственно, дате начала запуска теста. В тесте участвовали только новые пользователи.
Дату окончания теста выбрали с тем смыслом, чтобы отследить также события пользователей, зарегистрировавшихся в последний день в течении последующих 14 дней, соответственно с 2020-12-21 по 2021-01-04.
Проверим даты событий пользователей.
print('Участники recommender_system_test совершали события в период:', new_rec_participants['event_dt'].max()-new_rec_participants['event_dt'].min(),
'с', new_rec_participants['event_dt'].min(), 'по', new_rec_participants['event_dt'].max())
Участники recommender_system_test совершали события в период: 23 days 12:37:00 с 2020-12-07 00:05:57 по 2020-12-30 12:42:57
У нас нет данных для 5 дней. Возможно были выгружены не все данные или на самом деле тест остановили раньше времени по ТЗ.
Согласно ТЗ в тест должно быть отобрано 15% новых пользователей из региона EU. Отфильтруем всех EU пользователей, которые зарегистрировались по 21.12.2020 и посчитаем долю участников нашего теста от них.
eu_users = new_users.query('first_date <= "2020-12-21 00:00:00"')[new_users.query('first_date <= "2020-12-21 00:00:00"')['region'] == 'EU']['user_id'].nunique()
test_eu_users = new_rec_participants[new_rec_participants['region'] == 'EU']['user_id'].nunique()
print('Доля участников теста из EU от общего количества участников из EU: {:.2%}'.format(test_eu_users / eu_users))
Доля участников теста из EU от общего количества участников из EU: 15.00%
Доля участников из EU, участвующих в тесте от общего числа зарегистрировавшихся в период набора участников, соответствует ТЗ.
Ожидаемый эффект по ТЗ: "За 14 дней с момента регистрации пользователи покажут улучшение каждой метрики не менее, чем на 10%".
Т.е. для исследовательского анализа мы оставим только те события, которые пользователи совершали в указанный период.
Время проведения теста по ТЗ - 2020-12-07 по 2021-01-04.
Мы располагаем данными о совершенных событиях пользователями за период с 2020-12-07 по 2020-12-30.
Посмотрим какие кампании и когда проводились.
campaings.sort_values(by='start_dt')
| name | regions | start_dt | finish_dt | |
|---|---|---|---|---|
| 6 | Chinese New Year Promo | APAC | 2020-01-25 | 2020-02-07 |
| 1 | St. Valentine's Day Giveaway | EU, CIS, APAC, N.America | 2020-02-14 | 2020-02-16 |
| 8 | International Women's Day Promo | EU, CIS, APAC | 2020-03-08 | 2020-03-10 |
| 2 | St. Patric's Day Promo | EU, N.America | 2020-03-17 | 2020-03-19 |
| 3 | Easter Promo | EU, CIS, APAC, N.America | 2020-04-12 | 2020-04-19 |
| 7 | Labor day (May 1st) Ads Campaign | EU, CIS, APAC | 2020-05-01 | 2020-05-03 |
| 9 | Victory Day CIS (May 9th) Event | CIS | 2020-05-09 | 2020-05-11 |
| 11 | Dragon Boat Festival Giveaway | APAC | 2020-06-25 | 2020-07-01 |
| 4 | 4th of July Promo | N.America | 2020-07-04 | 2020-07-11 |
| 13 | Chinese Moon Festival | APAC | 2020-10-01 | 2020-10-07 |
| 12 | Single's Day Gift Promo | APAC | 2020-11-11 | 2020-11-12 |
| 5 | Black Friday Ads Campaign | EU, CIS, APAC, N.America | 2020-11-26 | 2020-12-01 |
| 0 | Christmas&New Year Promo | EU, N.America | 2020-12-25 | 2021-01-03 |
| 10 | CIS New Year Gift Lottery | CIS | 2020-12-30 | 2021-01-07 |
C 2020-12-25 по 2021-01-03 проводилась рождественско-новогодняя акция. Имеется пересечение с периодом проведения теста в 5 дней. Посмотрим как распределились события по дням.
new_rec_participants['event_date'] = new_rec_participants['event_dt'].dt.date # добавим столбец с датами
# посчитаем число событий для каждой даты
action_date = new_rec_participants.groupby('event_date').agg({'event_name':'count'}).reset_index()
action_date.head()
| event_date | event_name | |
|---|---|---|
| 0 | 2020-12-07 | 709 |
| 1 | 2020-12-08 | 593 |
| 2 | 2020-12-09 | 746 |
| 3 | 2020-12-10 | 613 |
| 4 | 2020-12-11 | 542 |
sns.set(rc = {'figure.figsize':(20,8)})
sns.barplot(x = 'event_date',
y = 'event_name',
data = action_date,
ci = 0,
palette='seismic')
plt.title('Динамика событий по дням', fontsize=20)
plt.ylabel('Количество событий', fontsize=15)
plt.xlabel('Дата', fontsize=15)
plt.xticks(rotation=45)
plt.show()
В целом мы наблюдаем повышение активности в предпраздничные дни. На графике наблюдается два особенных пика событий 14 декабря и 21 декабря. С 21 декабря акивность пошла на спад. С 25 декабря особенной активности не наблюдается. Поэтому мы можем сделать вывод, что акция не повлияла на поведение пользователей. Значит с маркетинговыми активностями всплески не связаны, возможно есть другая причина. Посмотрим на динамику регистрации новых пользователей по тестовым группам.
# выведем для каждой даты регистрации каждого пользователя
users_date = new_rec_participants.groupby(['first_date', 'group']).agg({'user_id':'count'}).reset_index()
users_date['first_date'] = users_date['first_date'].dt.date
users_date.head()
| first_date | group | user_id | |
|---|---|---|---|
| 0 | 2020-12-07 | A | 1301 |
| 1 | 2020-12-07 | B | 1489 |
| 2 | 2020-12-08 | A | 795 |
| 3 | 2020-12-08 | B | 373 |
| 4 | 2020-12-09 | A | 636 |
sns.set(rc = {'figure.figsize':(20,8)})
sns.barplot(x = 'first_date',
y = 'user_id',
hue = 'group',
data = users_date,
estimator=sum,
ci = 0,
palette='seismic')
plt.title('Динамика регистраций участников по дням', fontsize=20)
plt.legend(title = 'Группа', fontsize=15)
plt.ylabel('Количество регистраций', fontsize=15)
plt.xlabel('Дата', fontsize=15)
plt.xticks(rotation=45)
plt.show()
График показывает, что именно 14 и 21 декабря шел активный набор группы А. Причину мы можем только предполагать.
# создадим срез с конкурирующим тестом
inter_participants = participants.query('ab_test == "interface_eu_test"')
inter_participants
| user_id | group | ab_test | |
|---|---|---|---|
| 6701 | D4E530F6595A05A3 | A | interface_eu_test |
| 6702 | 773ECB64E45DEBAB | A | interface_eu_test |
| 6703 | 6BCB0F33D3BAB8C2 | A | interface_eu_test |
| 6704 | AABA4219186465C9 | A | interface_eu_test |
| 6705 | 2BA8FA8754D1FE50 | B | interface_eu_test |
| ... | ... | ... | ... |
| 18263 | 1D302F8688B91781 | B | interface_eu_test |
| 18264 | 3DE51B726983B657 | A | interface_eu_test |
| 18265 | F501F79D332BE86C | A | interface_eu_test |
| 18266 | 63FBE257B05F2245 | A | interface_eu_test |
| 18267 | 79F9ABFB029CF724 | B | interface_eu_test |
11567 rows × 3 columns
Посчитаем сколько пересечений между тестами.
print(len(np.intersect1d(new_rec_participants['user_id'].unique(), inter_participants['user_id'].unique())), 'пользователей принимали участие в обоих тестированиях.')
1602 пользователей принимали участие в обоих тестированиях.
В таком случае конкурирующий тест может повлиять на пользователей из нашего теста. Посмотрим доли таких пользователей в группах нашего теста.
round(len(np.intersect1d(new_rec_participants.query('group == "A"')['user_id'].unique(), inter_participants['user_id'].unique())) / new_rec_participants.query('group == "A"')['user_id'].nunique() * 100, 2)
24.08
round(len(np.intersect1d(new_rec_participants.query('group == "B"')['user_id'].unique(), inter_participants['user_id'].unique())) / new_rec_participants.query('group == "B"')['user_id'].nunique() * 100, 2)
23.67
В группе А нашего теста пересекающиеся участники занимают около 24 % от общего числа. В группе В - также приблизительно 24 %. Есть основание считать эти доли приблизительно равными. Соответственно, участники из двух тестов распределены равномерно по группам нашего теста, основания их удалять нет.
len(np.intersect1d(new_rec_participants.query('group == "A"')['user_id'].unique(),
new_rec_participants.query('group == "B"')['user_id'].unique()))
0
Пользователей, попавших в обе группы нашего теста не обнаружено.
Мы уже пришли к выводу, что не все пользователи совершали события, так как в таблице с событиями уникальных id меньше. Проверим, все ли пользователи, зарегистрированные в нашем тесте, совершали за период проведения теста события.
print('Доля активных участников всего теста: {:.2%}'.format(len(np.intersect1d(new_rec_participants['user_id'].unique(),
events['user_id'].unique())) / new_rec_participants['user_id'].nunique()))
Доля активных участников всего теста: 54.84%
Посмотрим как активные пользователи распределились по группам.
print('Доля активных участников теста среди группы А: {:.2%}'.format(len(np.intersect1d(new_rec_participants.query('group == "A"')['user_id'].unique(),
events['user_id'].unique())) / new_rec_participants.query('group == "A"')['user_id'].nunique()))
Доля активных участников теста среди группы А: 71.84%
print('Доля активных участников теста среди группы B: {:.2%}'.format(len(np.intersect1d(new_rec_participants.query('group == "B"')['user_id'].unique(),
events['user_id'].unique())) / new_rec_participants.query('group == "B"')['user_id'].nunique()))
Доля активных участников теста среди группы B: 32.26%
Возможно, учитывая условие по ТЗ о 14 -дневном лайфтайме, мы отфильтруем невошедшие в него события, и, свместе с тем неактивных в этот период пользователей.
# отфильтруем события с лайфтаймом больше 14 дней
new_rec_participants = new_rec_participants[(new_rec_participants['event_dt'] - new_rec_participants['first_date']).dt.days <= 14]
print('Доля оставшихся после фильтрации пользователей: {:.2%}'.format(new_rec_participants['user_id'].nunique() / len(rec_participants)))
Доля оставшихся после фильтрации пользователей: 54.84%
Осталось около 55 % пользователей. Теперь проверим изменилась ли ситуация по неактивным пользователям.
print('Доля активных участников всего теста: {:.2%}'.format(len(np.intersect1d(new_rec_participants['user_id'].unique(),
events['user_id'].unique())) / new_rec_participants['user_id'].nunique()))
Доля активных участников всего теста: 100.00%
Все неактивные участники теста отфильтровались с событиями, не входящими в 14 -дневный лайфтайм каждого участника.
Теперь посмотрим на итоговое распределение между группами нашего теста.
new_rec_participants.shape
(24070, 10)
new_rec_participants['user_id'].nunique()
3675
new_rec_participants.groupby(['ab_test', 'group']).agg({'user_id': 'nunique'})
| user_id | ||
|---|---|---|
| ab_test | group | |
| recommender_system_test | A | 2747 |
| B | 928 |
Пользователи также наравномерно распределены по группам теста. Перепроверим долю участников из EU.
eu_users = new_users.query('first_date <= "2020-12-21 00:00:00"')[new_users.query('first_date <= "2020-12-21 00:00:00"')['region'] == 'EU']['user_id'].nunique()
test_eu_users = new_rec_participants[new_rec_participants['region'] == 'EU']['user_id'].nunique()
print('Доля участников теста из EU от общего количества участников из EU: {:.2%}'.format(test_eu_users / eu_users))
Доля участников теста из EU от общего количества участников из EU: 8.22%
В целом тест проведен с нарушенияем ряда условий, что может привести к некорректному результату:
Разделение участников по группам теста неравномерно
У нас нет данных для 5 последних дней до даты окончания теста по ТЗ. Возможно были выгружены не все данные или на самом деле тест остановили раньше времени по ТЗ
в период проведения теста проводилась акция, она не повлияла особенным образом на пользовательскую активность, но рекомендуем не проводить акции паралллельно с тестированием
Динамика событий по дням неравномерна, так как период проведения теста пришелся на рожденственские праздники, что повлияло на поведение участников теста. Также наблюдается повышенная активность пользователей группы А - 14 и 21 декабря, что рекомендуем изучить подробнее, проконсультировавшись с отделом маркетинга
мы выявили пересечение участников с конкурирующим тестом, что оказывает влияние на участников нашего теста. Распределение долей по группам нашего теста показало примерно одинаковую долю общих с конкурирующим тестом участников, мы делаем вывод, что повлиял он на них в одинаковой степени. Рекомендуем отслеживать состав групп в будущих тестированиях.
в тесте участвовало всего около 55 % активных пользователей. После фильтрации дат событий, входящих в 14-дневный лайфтайм каждого пользователя, неактивные участники отсеялись, но вместе с тем снизилась доля участников из EU от общего числа новых пользователей до 8,22 %. При нефильтрованных данных доля соответствовала ТЗ.
Посчитаем среднее количество событий на участника.
print('Среднее количество событий на пользователя: ', round(len(new_rec_participants['event_name']) / new_rec_participants['user_id'].nunique()))
Среднее количество событий на пользователя: 7
# посчитаем число событий для каждого пользователя
users_events = new_rec_participants.groupby(['user_id', 'group']).agg({'event_name':'count'}).reset_index()
users_events.head()
| user_id | group | event_name | |
|---|---|---|---|
| 0 | 001064FEAAB631A1 | B | 6 |
| 1 | 0010A1C096941592 | A | 12 |
| 2 | 00341D8401F0F665 | A | 2 |
| 3 | 003DF44D7589BBD4 | A | 15 |
| 4 | 00505E15A9D81546 | A | 5 |
sns.set(rc = {'figure.figsize':(20,8)})
sns.boxplot(data = users_events, y='event_name', x=users_events['group'].sort_values(), palette='seismic')
plt.title('Распределение количества событий на пользователя в группах.', fontsize=20)
plt.xlabel('Группы', fontsize=15)
plt.ylabel('Количество событий на пользователя', fontsize=15)
plt.suptitle('')
plt.show()
users_events[users_events['group'] == 'A']['event_name'].describe()
count 2747.000000 mean 6.897343 std 3.840267 min 1.000000 25% 4.000000 50% 6.000000 75% 9.000000 max 24.000000 Name: event_name, dtype: float64
users_events[users_events['group'] == 'B']['event_name'].describe()
count 928.000000 mean 5.520474 std 3.303036 min 1.000000 25% 3.000000 50% 4.000000 75% 8.000000 max 24.000000 Name: event_name, dtype: float64
Среднее количество событий на пользователя в группе А - около 7(медиана - 6), в группе В - около 5(медиана - 4). Минимальные и мксимальные значения в выборках совпадают. Стандартное отклонение примерно одинаково. Можно сделать вывод, что события в обеих выборках распределены почти одинаково.
#создадим группировку по датам и посчитаем среднее количество событий на пользователя по дням
date_events = new_rec_participants.groupby(['event_date', 'group']).agg({'event_name':'count', 'user_id':'nunique'}).reset_index()
date_events['users_mean_events'] = round(date_events['event_name'] / date_events['user_id'], 2)
date_events.head()
| event_date | group | event_name | user_id | users_mean_events | |
|---|---|---|---|---|---|
| 0 | 2020-12-07 | A | 331 | 154 | 2.15 |
| 1 | 2020-12-07 | B | 378 | 173 | 2.18 |
| 2 | 2020-12-08 | A | 341 | 160 | 2.13 |
| 3 | 2020-12-08 | B | 252 | 120 | 2.10 |
| 4 | 2020-12-09 | A | 385 | 178 | 2.16 |
sns.set(rc = {'figure.figsize':(20,8)})
sns.barplot(x = 'event_date',
y = 'users_mean_events',
hue = 'group',
data = date_events,
ci = 0,
palette='seismic')
plt.title('Динамика среднего количества событий участника по дням', fontsize=20)
plt.legend(title = 'Группа', fontsize=15)
plt.ylabel('Количество событий', fontsize=15)
plt.xlabel('Дата', fontsize=15)
plt.xticks(rotation=45)
plt.show()
Мы набюдаем рост количества событий на пользователя в группе А с 14 и с 24 декабря. Выше мы выявили, что 14 декабря шел активный набор в группу А. Причину того, что могло повлиять на динамику с 24 декабря мы не можем определить , рекомендуем проконсультироваться с отделом маркетинга.
Посмотрим, какие события есть в логах, как часто они встречаются.
event_grouped = (
new_rec_participants.pivot_table(index = 'event_name', columns='group', values='user_id', aggfunc='nunique')
.reset_index()
.reindex([3,0,2,1])
)
event_grouped['total_events'] = event_grouped['A'] + event_grouped['B']
event_grouped['share%'] = round(event_grouped['total_events'] / sum(event_grouped['total_events']) * 100, 2)
event_grouped
| group | event_name | A | B | total_events | share% |
|---|---|---|---|---|---|
| 3 | регистрация | 2747 | 927 | 3674 | 44.89 |
| 0 | конверсии в просмотр карточек товаров | 1780 | 523 | 2303 | 28.14 |
| 2 | просмотр корзины | 824 | 255 | 1079 | 13.18 |
| 1 | покупка | 872 | 256 | 1128 | 13.78 |
На регистрацию приходится 45 % всех событий. Около 28 % событий -это конверсии в просмотр карточек товаров. Примерно одинаковые доли приходятся на просмотр корзины и покупку - около 13 %.
event_grouped_a = (
new_rec_participants.query('group == "A"')
.groupby('event_name')
.agg({'event_name':'count', 'user_id':'nunique'})
)
event_grouped_a.columns = ['events', 'users_unique']
event_grouped_a['share%'] = round((event_grouped_a['users_unique'] / new_rec_participants['user_id'].nunique() * 100), 2)
event_grouped_a = event_grouped_a.reset_index().reindex([3,0,2,1])
event_grouped_a
| event_name | events | users_unique | share% | |
|---|---|---|---|---|
| 3 | регистрация | 8400 | 2747 | 74.75 |
| 0 | конверсии в просмотр карточек товаров | 5415 | 1780 | 48.44 |
| 2 | просмотр корзины | 2519 | 824 | 22.42 |
| 1 | покупка | 2613 | 872 | 23.73 |
event_grouped_b = (
new_rec_participants.query('group == "B"')
.groupby('event_name')
.agg({'event_name':'count', 'user_id':'nunique'})
.sort_values(by='user_id', ascending=False)
)
event_grouped_b.columns = ['events', 'users_unique']
event_grouped_b['share%'] = round((event_grouped_b['users_unique'] / new_rec_participants['user_id'].nunique() * 100), 2)
event_grouped_b = event_grouped_b.reset_index().reindex([0,1,3,2])
event_grouped_b
| event_name | events | users_unique | share% | |
|---|---|---|---|---|
| 0 | регистрация | 2493 | 927 | 25.22 |
| 1 | конверсии в просмотр карточек товаров | 1331 | 523 | 14.23 |
| 3 | просмотр корзины | 659 | 255 | 6.94 |
| 2 | покупка | 640 | 256 | 6.97 |
Построим диаграмму воронки событий.
fig = go.Figure()
fig.add_trace(go.Funnel(
name = 'Группа А',
y = event_grouped_a['event_name'],
x = event_grouped_a['users_unique'],
textinfo = "value+percent initial"))
fig.add_trace(go.Funnel(
name = 'Группа В',
orientation = "h",
y = event_grouped_b['event_name'],
x = event_grouped_b['users_unique'],
textposition = "inside",
textinfo = "value+percent previous"))
fig.update_layout(title='Воронка пользователей по событиям')
Можем предположить, что примерный путь пользователей группы А и группы В выглядит так:
Регистрация
Переход в просмотр карточек товаров
Просмотр корзины
Покупка
Можно предположить, что есть возможность покупки, минуя корзину. Поэтому конверсий в корзину меньше.
Также можем отметить, что около 32 % пользователей в группе А и около 28 % в группе В, зарегистрировавшихся пользователей доходит до страницы с успешной оплатой.
Посчитаем , какая доля пользователей проходит на следующий шаг воронки от числа пользователей на предыдущем.
users_share_a = event_grouped_a.loc[:, ('event_name', 'users_unique')]
users_share_a['share_after_step'] = round((users_share_a['users_unique'] / users_share_a['users_unique'].shift())[1:], 2)
users_share_a
| event_name | users_unique | share_after_step | |
|---|---|---|---|
| 3 | регистрация | 2747 | NaN |
| 0 | конверсии в просмотр карточек товаров | 1780 | 0.65 |
| 2 | просмотр корзины | 824 | 0.46 |
| 1 | покупка | 872 | 1.06 |
users_share_b = event_grouped_b.loc[:, ('event_name', 'users_unique')]
users_share_b['share_after_step'] = round((users_share_b['users_unique'] / users_share_b['users_unique'].shift())[1:], 2)
users_share_b
| event_name | users_unique | share_after_step | |
|---|---|---|---|
| 0 | регистрация | 927 | NaN |
| 1 | конверсии в просмотр карточек товаров | 523 | 0.56 |
| 3 | просмотр корзины | 255 | 0.49 |
| 2 | покупка | 256 | 1.00 |
Группа А:
Группа В:
Можно утвеждать, что на шаге страница карточек товаров - просмотр корзины теряется больше всего пользователей-около 51 %.
В целом можно сделать вывод, что воронки событий обеих групп одинаковы по порядку этапов, в обеих группах наблюдаются большие потери пользователей(более 50 %) на шаге страница карточек товаров - просмотр корзины. Рекомендуем обратить внимание на то, почему достаточно большое количество пользователей не переходят на страницу с корзиной.
Конверсия в этап страница с каталогом ниже в тестовой группе на 9 %.
события в обеих выборках распределены почти одинаково
наблюдаются скачки событий 14 и 21 декабря. Возможно это связано с активным набором в группу А. Также заметно влияние рождественских праздников на днамику
мы выяснили, что переходов в корзину меньше, чем в покупки и в общем и при делении на группы теста. Возможно переход в покупки осуществлялся, минуя корзину. В связи с этим мы решили считать порядок этапов в логическом порядке:
Регистрация
Переход в просмотр карточек товаров
Просмотр корзины
Покупка
Нам необходимо проверить, находят ли статистические критерии разницу между долям в группах А и В при конверсии в этапы событий.
Нулевая гипотеза: различий между долями пользователей, совершивших событие, нет
Альтернативная гипотеза: различия между долями пользователей, совершивших событие, есть.
Чтобы снизить вероятность ложнопозитивного результата при множественном тестировании гипотез, мы применим поправку Бонферрони. Данные двух мы сравним по трем событиям (конверсии в просмотре карточек товаров, в просмотр корзины, в покупки т.к. на шаге регистрация 100% пользователей и сокращать из-за этого alpha не нужно).
Следовательно, bonferroni_alpha будет равно alpha / 3. В нашем случае мы будем использовать proportions_ztest для двух выборок. Мы сравниваем пропорции из этих выборках на разных этапах.
event_grouped_ab = new_rec_participants.pivot_table(index = 'event_name',
columns = 'group', values = 'user_id', aggfunc = 'nunique')
event_grouped_ab
| group | A | B |
|---|---|---|
| event_name | ||
| конверсии в просмотр карточек товаров | 1780 | 523 |
| покупка | 872 | 256 |
| просмотр корзины | 824 | 255 |
| регистрация | 2747 | 927 |
alpha = 0.05
print('bonferroni_alpha =', round(alpha/3, 6))
bonferroni_alpha = 0.016667
def p_z_test(successes1, successes2, trials1, trials2, bonferroni_alpha = 0.016667):
counts = [successes1, successes2]
nobs = [trials1, trials2]
print(counts, nobs)
stat, p_value = proportions_ztest(count = counts, nobs = nobs, alternative = 'two-sided')
print('z_stat: %0.3f, p_value: %0.6f' % (stat, p_value))
if p_value < bonferroni_alpha:
print('Отвергаем нулевую гипотезу: между долями есть разница')
else:
print(
'Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными')
users_a = new_rec_participants.query('group == "A"')['user_id'].nunique() #количество уникальных пользователей гр.А
users_b = new_rec_participants.query('group == "B"')['user_id'].nunique() #количество уникальных пользователей гр.В
for event in new_rec_participants.query('event_name != "регистрация"')['event_name'].unique():
print('Событие:', event)
p_z_test(event_grouped_ab.loc[event, 'A'],
event_grouped_ab.loc[event, 'B'],
users_a,
users_b)
print()
Событие: покупка [872, 256] [2747, 928] z_stat: 2.374, p_value: 0.017592 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Событие: просмотр корзины [824, 255] [2747, 928] z_stat: 1.456, p_value: 0.145348 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Событие: конверсии в просмотр карточек товаров [1780, 523] [2747, 928] z_stat: 4.596, p_value: 0.000004 Отвергаем нулевую гипотезу: между долями есть разница
Можно сделать вывод, что различий между долями уникальных пользователей среди групп А и В при конверсии в просмотр корзины и в покупку нет.
Мы оценили корректность проведения теста и проанализировали результаты теста.
Мы считаем, что тест был проведен некорректно:
У нас нет данных для 5 последних дней до даты окончания теста по ТЗ. Возможно были выгружены не все данные или на самом деле тест остановили раньше времени по ТЗ
в период проведения теста проводилась акция, она не повлияла особенным образом на пользовательскую активность, но рекомендуем не проводить акции паралллельно с тестированием
Динамика событий по дням неравномерна, так как период проведения теста пришелся на рожденственские праздники, что повлияло на поведение участников теста. Также наблюдается повышенная активность пользователей группы А - 14 и 21 декабря, что совпадает с активным набором пользователей в группу А, рекомендуем изучить этот вопрос подробнее, проконсультировавшись с отделом маркетинга.
Параллельно с нашим тестом проводился другой тест. 1602 пользователей принимали участие в обоих тестированиях, что по нашим расчетам возможно не повлияло на результаты, так как доли подобных пользователей распределились равномерно между группами и одинаково повлияли на каждую из них. Настоятельно рекомендуем не проводить параллельно несколько тестов, это может привести к искажению результатов.
В тесте recommender_system_test участвовал 6701 пользователь. Ожидаемое количество участников теста по ТЗ - 6000. После удаления участников, не совершавших события, мы получили всего 3675 пользователей.
Распределение по группам теста выглядит неравномерным. Разница почти в 1000 пользователей по начальным данным и 1819 после фильтрации неактивных пользователей. Рекомендуем отслеживать наполняемость групп.
Доля участников из EU, участвующих в тесте от общего числа зарегистрировавшихся в период набора участников, по сырым данным соответствует ТЗ (15%). После фильтрации неактивных пользватлей доля участников из EU снизилась до 8,22 %.
Анализ воронки событий выявил, что:
не отслеживается логичный путь регистрация-просмотр карточки товара-корзина-оплата. Конверсий на этап просмотр корзины меньше, чем в оплату. Возможная причина-переход к оплате , минуя этап корзина. Этот момент повлиял на расчеты конверсий.
в обеих группах наблюдаются большие потери пользователей(более 50 %) на шаге страница карточек товаров - просмотр корзины. Рекомендуем обратить внимание на то, почему достаточно большое количество пользователей не переходят на страницу с корзиной.
Также можем отметить, что около 32 % пользователей в группе А и около 28 % в группе В, зарегистрировавшихся пользователей доходит до страницы с успешной оплатой.
Конверсия в этап страница с каталогом ниже в тестовой группе на 9 %.
Проверка гипотезы о равенстве долей показала, что что различий между долями уникальных пользователей среди групп А и В при конверсии в просмотр корзины и в покупку нет. Это значит, что нововведение не повлияло каким -либо образом на конверсию на этих этапах.
Обнаружена разница между долями на этапе просмотр карточек товаров. Доля потерянных пользователей с этапа регистрация у группы А около 35 %, у группы В-около 44 %. Соответственно можно сделать вывод, что группа В проигрывает-нововведение не сработало лучшим образом. Следовательно, внедрение улучшенной рекомендательной системы не влияет положительным образом на метрики, а в некоторых случаях, имеет более низкие показатели в сравнении с контрольной группой.
Некорректное проведение теста, могло исказить результаты нашего исследования. Рекомендуем перезапустить тест, учитывая моменты, освещенные в выводе.